using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Linq;

namespace System.Threading.Tasks
{
    public static class TaskEx
    {
        private static readonly Task PreCompletedTask = FromResult(false);
        private static readonly Task PreCanceledTask = ((Func<Task>)(() =>
        {
            var tcs = new TaskCompletionSource<bool>();
            tcs.TrySetCanceled();
            return (Task)tcs.Task;
        }))();

        private const string ArgumentOutOfRangeTimeoutNonNegativeOrMinusOne = "The timeout must be non-negative or -1, and it must be less than or equal to Int32.MaxValue.";

        static TaskEx()
        {
        }

        public static Task Run(Action action)
        {
            return Run(action, CancellationToken.None);
        }

        public static Task Run(Action action, CancellationToken cancellationToken)
        {
            return Task.Factory.StartNew(action, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default);
        }

        public static Task<TResult> Run<TResult>(Func<TResult> function)
        {
            return Run(function, CancellationToken.None);
        }

        public static Task<TResult> Run<TResult>(Func<TResult> function, CancellationToken cancellationToken)
        {
            return Task.Factory.StartNew(function, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default);
        }

        public static Task Run(Func<Task> function)
        {
            return Run(function, CancellationToken.None);
        }

        public static Task Run(Func<Task> function, CancellationToken cancellationToken)
        {
            return Run<Task>(function, cancellationToken).Unwrap();
        }

        public static Task<TResult> Run<TResult>(Func<Task<TResult>> function)
        {
            return Run(function, CancellationToken.None);
        }

        public static Task<TResult> Run<TResult>(Func<Task<TResult>> function, CancellationToken cancellationToken)
        {
            return Run<Task<TResult>>(function, cancellationToken).Unwrap();
        }

        public static Task Delay(int dueTime)
        {
            return Delay(dueTime, CancellationToken.None);
        }

        public static Task Delay(TimeSpan dueTime)
        {
            return Delay(dueTime, CancellationToken.None);
        }

        public static Task Delay(TimeSpan dueTime, CancellationToken cancellationToken)
        {
            var timeoutMs = (long)dueTime.TotalMilliseconds;
            if (timeoutMs < Timeout.Infinite || timeoutMs > int.MaxValue)
                throw new ArgumentOutOfRangeException("dueTime", ArgumentOutOfRangeTimeoutNonNegativeOrMinusOne);

            return Delay((int)timeoutMs, cancellationToken);
        }

        public static Task Delay(int dueTime, CancellationToken cancellationToken)
        {
            if (dueTime < -1)
                throw new ArgumentOutOfRangeException("dueTime", ArgumentOutOfRangeTimeoutNonNegativeOrMinusOne);
            if (cancellationToken.IsCancellationRequested)
                return PreCanceledTask;
            if (dueTime == 0)
                return PreCompletedTask;
            var tcs = new TaskCompletionSource<bool>();
            var ctr = new CancellationTokenRegistration();

            Timer timer = null;
            timer = new Timer(_ =>
            {
                ctr.Dispose();
                timer.Dispose();
                tcs.TrySetResult(true);
            }, null, Timeout.Infinite, Timeout.Infinite);

            if (cancellationToken.CanBeCanceled)
            {
                ctr = cancellationToken.Register(() =>
                {
                    timer.Dispose();
                    tcs.TrySetCanceled();
                });
            }

            timer.Change(dueTime, Timeout.Infinite);
            return tcs.Task;
        }

        public static Task WhenAll(params Task[] tasks)
        {
            return WhenAll((IEnumerable<Task>)tasks);
        }

        public static Task<TResult[]> WhenAll<TResult>(params Task<TResult>[] tasks)
        {
            return WhenAll((IEnumerable<Task<TResult>>)tasks);
        }

        public static Task WhenAll(IEnumerable<Task> tasks)
        {
            return WhenAllCore(tasks, (Action<Task[], TaskCompletionSource<object>>)((completedTasks, tcs) => tcs.TrySetResult(null)));
        }

        public static Task<TResult[]> WhenAll<TResult>(IEnumerable<Task<TResult>> tasks)
        {
            return WhenAllCore<TResult[]>(tasks.Cast<Task>(), (completedTasks, tcs) =>
                                                 tcs.TrySetResult(completedTasks
                                                                      .Cast<Task<TResult>>()
                                                                      .Select(t => t.Result)
                                                                      .ToArray()));
        }

        private static Task<TResult> WhenAllCore<TResult>(IEnumerable<Task> tasks, Action<Task[], TaskCompletionSource<TResult>> setResultAction)
        {
            if (tasks == null)
                throw new ArgumentNullException("tasks");
            var tcs = new TaskCompletionSource<TResult>();
            var taskArray = tasks as Task[] ?? tasks.ToArray();
            if (taskArray.Length == 0)
                setResultAction(taskArray, tcs);
            else
                Task.Factory.ContinueWhenAll(taskArray, completedTasks =>
                {
                    List<Exception> exceptions = null;
                    var canceled = false;
                    foreach (var task in completedTasks)
                    {
                        if (task.IsFaulted)
                            AddPotentiallyUnwrappedExceptions(ref exceptions, task.Exception);
                        else if (task.IsCanceled)
                            canceled = true;
                    }
                    if (exceptions != null && exceptions.Count > 0)
                        tcs.TrySetException(exceptions);
                    else if (canceled)
                        tcs.TrySetCanceled();
                    else
                        setResultAction(completedTasks, tcs);
                }, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
            return tcs.Task;
        }

        public static Task<Task> WhenAny(params Task[] tasks)
        {
            return WhenAny((IEnumerable<Task>)tasks);
        }

        public static Task<Task> WhenAny(IEnumerable<Task> tasks)
        {
            if (tasks == null)
                throw new ArgumentNullException("tasks");
            var tcs = new TaskCompletionSource<Task>();
            Task.Factory.ContinueWhenAny<bool>(tasks as Task[] ?? tasks.ToArray(), tcs.TrySetResult, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
            return tcs.Task;
        }

        public static Task<Task<TResult>> WhenAny<TResult>(params Task<TResult>[] tasks)
        {
            return WhenAny((IEnumerable<Task<TResult>>)tasks);
        }

        public static Task<Task<TResult>> WhenAny<TResult>(IEnumerable<Task<TResult>> tasks)
        {
            if (tasks == null)
                throw new ArgumentNullException("tasks");
            var tcs = new TaskCompletionSource<Task<TResult>>();
            Task.Factory.ContinueWhenAny<TResult, bool>(tasks as Task<TResult>[] ?? tasks.ToArray(), tcs.TrySetResult, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
            return tcs.Task;
        }

        public static Task<TResult> FromResult<TResult>(TResult result)
        {
            var completionSource = new TaskCompletionSource<TResult>(result);
            completionSource.TrySetResult(result);
            return completionSource.Task;
        }

        public static YieldAwaitable Yield()
        {
            return new YieldAwaitable();
        }

        private static void AddPotentiallyUnwrappedExceptions(ref List<Exception> targetList, Exception exception)
        {
            var aggregateException = exception as AggregateException;
            if (targetList == null)
                targetList = new List<Exception>();
            if (aggregateException != null)
                targetList.Add(aggregateException.InnerExceptions.Count == 1 ? exception.InnerException : exception);
            else
                targetList.Add(exception);
        }
    }
}
